/*
 * Copyright 2012 Finbudget.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package finbudget.client.lists;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.google.web.bindery.event.shared.EventBus;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.event.shared.HasHandlers;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;
import com.gwtplatform.dispatch.shared.DispatchAsync;

import finbudget.client.event.ShowErrorMessageEvent;
import finbudget.client.lists.GetListOfItems.BudgetListItem;
import finbudget.client.lists.GetListOfItems.ExtendedBudgetListItem;
import finbudget.client.lists.GetListOfItems.ExtendedListType;
import finbudget.client.lists.GetListOfItems.FilteredListType;
import finbudget.client.lists.GetListOfItems.LinkedListType;
import finbudget.client.lists.GetListOfItems.ListType;
import finbudget.client.lists.GetListOfItems.PeriodListType;
import finbudget.client.lists.NewItemAddedToListEvent.NewItemAddedToListHandler;
import finbudget.client.ui.table.Record;
import finbudget.shared.Constants;

/**
 *         The only instance of this class is intended to keep all
 *         the lists (for comboboxes, oracles etc) for the application on client side.
 *         Initially all the lists are empty. They are filled by
 *         values on demand.
 *         The class is also listen for events firing when new
 *         list items are added.
 *         The class fires itself the event to notify the listeners
 *         on data arrived from the server.
 *         About events: http://code.google.com/p/google-web-toolkit-incubator/wiki/GwtEventSystem
 */
public class ListsManager implements NewItemAddedToListHandler, HasHandlers {
	private final Map<ListType, List<BudgetListItem>> map;
	private final Map<ListType, String> mapListNames;
	private final HandlerManager handlerManager;
	private final DispatchAsync dispatcher;
	private final EventBus eventBus;

	@Inject
	protected ListsManager(DispatchAsync dispatcher, EventBus eventBus) {
		this.dispatcher = dispatcher;
		map = new HashMap<ListType, List<BudgetListItem>>(50);
		mapListNames = new HashMap<ListType, String>(50);
		handlerManager = new HandlerManager(this);
		this.eventBus = eventBus;
		eventBus.addHandler(NewItemAddedToListEvent.TYPE, this);
	}

	public List<BudgetListItem> getList(ListType listType) {
		List<BudgetListItem> list = map.get(listType);
		if (list == null) {
			list = new ArrayList<BudgetListItem>();
			map.put(listType, list);
			mapListNames.put(listType, "?");
		}
		return list;
	}

	public final void requestList(final ListType type) {
		final List<BudgetListItem> list = getList(type);
		if (list.size() > 0) {
			// Requested list is not empty. Fire events.
			ListRequestedEvent requestedEvent = new ListRequestedEvent(type);
			fireEvent(requestedEvent);
			ListArrivedEvent e = new ListArrivedEvent(type, list);
			fireEvent(e);
			return;
		}
		// The list is empty. Fill it.
		doRequest(type, list);
	}

	protected final void doRequest(final ListType type, final List<BudgetListItem> list) {
		doRequestByPartions(type, list, 0);
	}

	private final void doRequestByPartions(final ListType type, final List<BudgetListItem> list, final int offset) {
		if (offset == 0) {
			// Clear the counter
			type.setTimeToGetFromServer(0);
		}
		ListRequestedEvent requestedEvent = new ListRequestedEvent(type);
		fireEvent(requestedEvent);

		if (requestedEvent.isCanceled()) {
			// Request was canceled on the client and client provided list of items.
			list.clear();
			if (requestedEvent.getItems() != null) {
				list.addAll(requestedEvent.getItems());
			}
			mapListNames.put(type, type.base);
			ListArrivedEvent event = new ListArrivedEvent(type, list);
			fireEvent(event);
		} else {
			// Request the list on the server.
			final long startingTime = System.currentTimeMillis();
			dispatcher.execute(new GetListOfItemsAction(type, offset), new AsyncCallback<GetListOfItemsResult>(){
				@Override
				public void onFailure(Throwable caught) {
					eventBus.fireEvent(new ShowErrorMessageEvent("Error getting list of items", caught));
				}
				@Override
				public void onSuccess(GetListOfItemsResult result) {
					type.setTimeToGetFromServer(type.getTimeToGetFromServer() + System.currentTimeMillis() - startingTime);
					if (result != null && result.getList() != null) {
						if (offset == 0) {
							// new list started
							list.clear();
						}
						list.addAll(result.getList());
						if (result.getOffsetToContinue() > 0) {
							// List is not complete
							doRequestByPartions(type, list, result.getOffsetToContinue());
						} else {
							// Complete list
							mapListNames.put(type, result.getListName() == null ? "no name" : result.getListName());
							if (type.base == null) {
								type.base = result.getBase();
							}
							ListArrivedEvent event = new ListArrivedEvent(type, list);
							fireEvent(event);
						}
					} else {
						eventBus.fireEvent(new ShowErrorMessageEvent("No list of items", null));
					}
				}
			});
		}
	}

	public final void requestRefreshedList(ListType listType) {
		List<BudgetListItem> list = getList(listType);
		if (list.size() != 0) {
			// Clear the list, so force it to be renewed.
			list.clear();
		}
		requestList(listType);
	}

	@Override
	public final void onNewItemAdded(final NewItemAddedToListEvent event) {
//		List<BudgetListItem> list = getList(event.getListType());
		// It's better because in case of adding new item we are not
		// intended to see just one this item in the list.
		List<BudgetListItem> list = map.get(event.getListType());

		if (list != null) {
			list.add(event.getItem());
			ListArrivedEvent e = new ListArrivedEvent(event.getListType(), list);
			fireEvent(e);
		}
	}

	@Override
	public void fireEvent(GwtEvent<?> event) {
		handlerManager.fireEvent(event);
	}

	public HandlerRegistration addListArrivedEventHandler(ListArrivedHandler handler) {
		return handlerManager.addHandler(ListArrivedEvent.TYPE, handler);
	}

	public void removeListArrivedEventHandler(ListArrivedHandler handler) {
		handlerManager.removeHandler(ListArrivedEvent.TYPE, handler);
	}

	public HandlerRegistration addListRequestedEventHandler(ListRequestedHandler handler) {
		return handlerManager.addHandler(ListRequestedEvent.TYPE, handler);
	}

	public void removeListRequestedEventHandler(ListRequestedHandler handler) {
		handlerManager.removeHandler(ListRequestedEvent.TYPE, handler);
	}

	public Set<Entry<ListType, String>> getEntriesOfListTypeNames() {
		return mapListNames.entrySet();
	}

	public boolean isListOnClient(ListType type) {
		if (type != null) {
			String name = mapListNames.get(type);
			if (name != null && !"?".equals(name)) {
				return true;
			}
		}
		return false;
	}

	public int getNumberOfItemsInList(ListType type) {
		List<BudgetListItem> list = map.get(type);
		return list == null ? 0 : list.size();
	}

	public String getListName(ListType type) {
		return mapListNames.get(type);
	}

	/**
	 * This function must be called after a new element is added.
	 * @param record
	 */
	public void registerNewElement(Record record, String optionalBase) {
		if (record != null) {
			String base = record.getValueString(Constants.fieldNodeBase);
			if (base == null) {
				base = optionalBase;
			}
			if (base != null) {
				// Search for lists with the same base
				for (Entry<ListType, List<BudgetListItem>> entry : map.entrySet()) {
					ListType listType = entry.getKey();
					if (listType instanceof RefreshListFromServerOnNewItemsCreated && base.equals(listType.base)) {
						requestRefreshedList(listType);
					}
				}
			}
			String name = record.getValueString(Constants.fieldNodeName);
			String key = record.getKey();
			if (base != null && name != null && key != null) {
				// Search for lists with the same base
				for (Entry<ListType, List<BudgetListItem>> entry : map.entrySet()) {
					ListType listType = entry.getKey();
					if (!(listType instanceof RefreshListFromServerOnNewItemsCreated) && base.equals(listType.base)) {
						// New element is a candidate for this list
						BudgetListItem item = null;
						if (listType instanceof PeriodListType) {
							// We add item to the list of periods just in case the property "caltype" of new item
							// is the same as defined in listType
							PeriodListType periodList = (PeriodListType) listType;
							String caltype = record.getValueString(PeriodListType.fieldName);
							if (caltype != null && caltype.equals(periodList.periodType)) {
								item = new BudgetListItem();
							}
						} else if (listType instanceof LinkedListType) {
							item = new BudgetListItem();
						} else if (listType instanceof FilteredListType) {
							FilteredListType filter = (FilteredListType) listType;
							String value = record.getValueString(filter.filteredProperty);
							if (value != null) {
								boolean theSame = value.equals(filter.filteredValue);
								if (theSame ^ filter.isExclusive) {
									item = new BudgetListItem();
								}
							}
						} else if (listType instanceof ExtendedListType) {
							ExtendedBudgetListItem itemExt = new ExtendedBudgetListItem();
							itemExt.extendedValue = record.getValueString(((ExtendedListType)listType).extendedPropertyName);
							item = itemExt;
						} else {
							item = new BudgetListItem();
						}
						if (item != null) {
							if (listType.substitute != null) {
								item.name = record.getValueString(listType.substitute);
							} else {
								item.name = name;
							}
							if (item.name != null) {
								item.key = key;
								NewItemAddedToListEventDto eventDto = new NewItemAddedToListEventDto(listType, item);
								onNewItemAdded(new NewItemAddedToListEvent(eventDto));
							}
						}
					}
				}
			}
		}
	}

	public void requestNewListsWithTheBase(String base) {
		if (base != null) {
			for (Entry<ListType, List<BudgetListItem>> item : map.entrySet()) {
				if (base.equals(item.getKey().base)) {
					this.requestRefreshedList(item.getKey());
				}
			}
		}
	}
}
